/*
 * Copyright 2020-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.security.oauth2.server.authorization.web;

import java.io.IOException;
import java.util.Arrays;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.JwtClientAssertionAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.PublicClientAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretBasicAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretPostAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.JwtClientAssertionAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;

/**
 * A {@code Filter} that processes an authentication request for an OAuth 2.0 Client.
 * 一个过滤器，用于处理OAuth 2.0客户端的认证请求
 * 客户端认证(该过滤器的作用就是对请求传递过来的clientId,clientSecret进行校验，是否在授权端注册过)：
 * client_secret_basic：基于Basic消息头认证(将clientId,clientSecret以:拼接,通过base64加密封装到请求头中的Authorization属性)
 * client_secret_post：POST请求进行认证(将clientId和clientSecret放到请求体（表单）中去请求授权服务器接口。授权服务器会通过client_secret进行验证)
 * private_key_jwt： 基于JWT进行认证，请求方使用私钥对JWT签名，授权服务器使用对应公钥进行验签认证(客户端自己维护密钥对，使用私钥生成jwt，并将公钥暴露给授权服务器。授权服务器通过客户端的公钥验证jwt)
 * client_secret_jwt：基于JWT进行认证，对JWT使用客户端密码+签名算法 签名(客户端使用client_secret通过HMAC算法生成jwt，调用授权服务器接口。授权服务器会通过HMAC算法验证jwt)
 * none (public clients)：公共客户端，不需要认证 会存在安全隐患，如果增加了PKCE时，再对code_verifier校验的时候也校验了clientId认证 但是clientSecret密码不校验
 * PKCE(Proof Key for Code Exchange)流程：
 * PKCE是一种用于增强授权码模式安全性的方法，它可以防止恶意应用程序通过截获授权码和重定向URI来获得访问令牌。PKCE通过将随机字符串（code_verifier）
 * 和其SHA-256哈希值（code_challenge）与授权请求一起发送，确保访问令牌只能由具有相应code_verifier的应用程序使用，保障用户的安全性。
 * 问题：public客户端，其本身没有能力保存密钥信息（恶意攻击者可以通过反编译等手段查看到客户端的密钥client_secret，也就可以通过授权码code换取access_token，
 * 到这一步，恶意应用就可以拿着token请求资源服务器了）
 * 解决方法：PKCE协议本身是对OAuth 2.0的扩展，它和之前的授权码流程大体上是一致的，区别在于在向授权服务器的authorize endpoint请求时，需要额外的code_challenge
 * 和code_challenge_method参数；向token endpoint请求时，需要额外的code_verifier参数。最后授权服务器会对这三个参数进行对比验证， 通过后颁发令牌。
 * 由于网络不法分子这样的中间人虽然可以截获code_challenge，但是他并不能由code_challenge逆推code_verifier，只有客户端自己才知道这两个值。
 * 参考：https://zhuanlan.zhihu.com/p/625423020
 * @author Joe Grandja
 * @author Patryk Kostrzewa
 * @since 0.0.1
 * @see AuthenticationManager
 * @see JwtClientAssertionAuthenticationConverter
 * @see JwtClientAssertionAuthenticationProvider
 * @see ClientSecretBasicAuthenticationConverter
 * @see ClientSecretPostAuthenticationConverter
 * @see ClientSecretAuthenticationProvider
 * @see PublicClientAuthenticationConverter
 * @see PublicClientAuthenticationProvider
 * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-2.3">Section 2.3 Client Authentication</a>
 * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1">Section 3.2.1 Token Endpoint Client Authentication</a>
 */
public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter {
	private final AuthenticationManager authenticationManager;
	private final RequestMatcher requestMatcher;
	private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
	private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource =
			new WebAuthenticationDetailsSource();
	private AuthenticationConverter authenticationConverter;
	private AuthenticationSuccessHandler authenticationSuccessHandler = this::onAuthenticationSuccess;
	private AuthenticationFailureHandler authenticationFailureHandler = this::onAuthenticationFailure;

	/**
	 * Constructs an {@code OAuth2ClientAuthenticationFilter} using the provided parameters.
	 *
	 * @param authenticationManager the {@link AuthenticationManager} used for authenticating the client
	 * @param requestMatcher the {@link RequestMatcher} used for matching against the {@code HttpServletRequest}
	 */
	public OAuth2ClientAuthenticationFilter(AuthenticationManager authenticationManager,
			RequestMatcher requestMatcher) {
		Assert.notNull(authenticationManager, "authenticationManager cannot be null");
		Assert.notNull(requestMatcher, "requestMatcher cannot be null");
		this.authenticationManager = authenticationManager;
		//该路径匹配器是一个OrRequestMatcher 匹配：/oauth2/token /oauth2/introspect /oauth2/revoke /oauth2/device_authorization
		this.requestMatcher = requestMatcher;
		this.authenticationConverter = new DelegatingAuthenticationConverter(
				Arrays.asList(
						new JwtClientAssertionAuthenticationConverter(),
						new ClientSecretBasicAuthenticationConverter(),
						new ClientSecretPostAuthenticationConverter(),
						new PublicClientAuthenticationConverter()));
	}
	//每种客户端认证方式实际就是一个AuthenticationConverter搭配一个AuthenticationProvider组合而成，一个负责解析请求参数，一个负责处理请求参数，执行认证逻辑
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
        //判断路径是否适配该过滤器，否则跳过该filter，执行后续filter
		if (!this.requestMatcher.matches(request)) {
			filterChain.doFilter(request, response);
			return;
		}

		try {
			// 通过转换器AuthenticationConverter将请求对象HttpServletRequest根据参数的不同转换成不同的待认证的OAuth2ClientAuthenticationToken对象
			// 注意返回的都是OAuth2ClientAuthenticationToken对象，只不过里面封装的属性信息不一样而已
			Authentication authenticationRequest = this.authenticationConverter.convert(request);
			//返回的待认证的对象OAuth2ClientAuthenticationToken是AbstractAuthenticationToken的子类
			if (authenticationRequest instanceof AbstractAuthenticationToken) {
				//将WebAuthenticationDetails对象(含有ip和sessionId)封装到authenticationRequest的details属性中
				((AbstractAuthenticationToken) authenticationRequest).setDetails(
						this.authenticationDetailsSource.buildDetails(request));
			}
			//五种认证方式再不抛出异常的情况下，均会走该处理逻辑
			if (authenticationRequest != null) {
				//对authenticationRequest中的clientId格式进行校验
				validateClientIdentifier(authenticationRequest);
				//根据待认证的Authentication对象，选择合适的AuthenticationProvider认证处理器，但是Converter转化后返回的均是OAuth2ClientAuthenticationToken对象
				//那么认证器JwtClientAssertionAuthenticationProvider、ClientSecretAuthenticationProvider、PublicClientAuthenticationProvider均满足条件
				//providerManager中会循环遍历挨个执行，只要有一个有返回值，那么就中断
				// 这里是对传过来的client进行认证是否再授权服务器注册过，但是并没有校验颁发code时使用的客户端信息是否和当前传递的客户端信息一致
				Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
				//客户端认证成功后将返回的authenticationResult{客户端认证信息}结果设置到安全上下文中
				this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
			}
			//认证成功继续执行后续的过滤器进而获取token信息
			filterChain.doFilter(request, response);

		} catch (OAuth2AuthenticationException ex) {
			if (this.logger.isTraceEnabled()) {
				this.logger.trace(LogMessage.format("Client authentication failed: %s", ex.getError()), ex);
			}
			//客户端认证失败将异常信息返回到客户端
			this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
		}
	}

	/**
	 * Sets the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
	 * to an instance of {@link OAuth2ClientAuthenticationToken} used for authenticating the client.
	 *
	 * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
	 */
	public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
		this.authenticationConverter = authenticationConverter;
	}

	/**
	 * Sets the {@link AuthenticationSuccessHandler} used for handling a successful client authentication
	 * and associating the {@link OAuth2ClientAuthenticationToken} to the {@link SecurityContext}.
	 *
	 * @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling a successful client authentication
	 */
	public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
		Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
		this.authenticationSuccessHandler = authenticationSuccessHandler;
	}

	/**
	 * Sets the {@link AuthenticationFailureHandler} used for handling a failed client authentication
	 * and returning the {@link OAuth2Error Error Response}.
	 *
	 * @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling a failed client authentication
	 */
	public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
		Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
		this.authenticationFailureHandler = authenticationFailureHandler;
	}

	private void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) {
		//自己添加验证获取的用户信息 虽然在获取code过程进行了登录认证并且将认证的用户信息存入到了session中，而此处也是从session中获取用户信息，但是需要注意的是此时无sessionId
		//走到此处说明为获取token的请求，该请求再客户端是通过远程调用的方式进行的不依赖于浏览器，所以其请求中无cookie信息，也就无sessionId,无session信息 所以此处为null
		Authentication principal = SecurityContextHolder.getContext().getAuthentication();
		SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
		//此处将已认证的客户端信息存入到安全上线文中
		securityContext.setAuthentication(authentication);
		// 将安全上线文存储到当前线程中  经过该设置那么在之后的filter中通过SecurityContextHolder.getContext().getAuthentication()
		// 获取的将不再是已认证用户信息而是已认证客户端信息   所以从此出可以看出code执行链时是以认证用户信息为主 token执行链时是在code执行链之后，
		// 此时肯定已经经历过code执行链，也就是说此时用户已经经过认证了 所以token执行链的关注点就不再是用户的认证，而是客户端是否已经认证
		SecurityContextHolder.setContext(securityContext);
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(LogMessage.format("Set SecurityContextHolder authentication to %s",
					authentication.getClass().getSimpleName()));
		}
	}

	private void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException {

		SecurityContextHolder.clearContext();

		// TODO
		// The authorization server MAY return an HTTP 401 (Unauthorized) status code
		// to indicate which HTTP authentication schemes are supported.
		// If the client attempted to authenticate via the "Authorization" request header field,
		// the authorization server MUST respond with an HTTP 401 (Unauthorized) status code and
		// include the "WWW-Authenticate" response header field
		// matching the authentication scheme used by the client.

		OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
		if (OAuth2ErrorCodes.INVALID_CLIENT.equals(error.getErrorCode())) {
			httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
		} else {
			httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
		}
		// We don't want to reveal too much information to the caller so just return the error code
		OAuth2Error errorResponse = new OAuth2Error(error.getErrorCode());
		this.errorHttpResponseConverter.write(errorResponse, null, httpResponse);
	}

	private static void validateClientIdentifier(Authentication authentication) {
		//必须是OAuth2ClientAuthenticationToken类型的否则返回null
		if (!(authentication instanceof OAuth2ClientAuthenticationToken)) {
			return;
		}

		// As per spec, in Appendix A.1.
		// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#appendix-A.1
		// The syntax for client_id is *VSCHAR (%x20-7E):
		// -> Hex 20 -> ASCII 32 -> space
		// -> Hex 7E -> ASCII 126 -> tilde

		OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication;
		//获取封住的clientId信息
		String clientId = (String) clientAuthentication.getPrincipal();
		for (int i = 0; i < clientId.length(); i++) {
			char charAt = clientId.charAt(i);
			if (!(charAt >= 32 && charAt <= 126)) {
				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
			}
		}
	}

}
